1.Обзор системы | 2.Мой первый проект | 3.Модель | 4.Представление | 5.Табличное представление | 6.Объектно-реляционное связывание | 7.Контроллеры | 8.Приложение | 9.Расширенные возможности

Глава 6: Объектно-реляционное связывание

Объектно-реляционное связывание (Object-relational mapping, ORM) реализует привязку объектов Java к БД, позволяя описывать какие таблицы и столбцы БД будут использованы для хранения постоянного состояния объектов.
Системы объектно-реляционного связывания (Object/relational tools) позволяют разработчику сконцентироваться на работе с объектами Java. Они обеспечивают автоматическое связывание объектов с БД, включая автоматическую генерацию кода SQL для выборки и модификации данных в БД. При использовании таких средств нет необходимости написании кода для прямого взаимодействия с БД. Но привязку ваших объектов Java к БД все таки описать нужно.
OpenXava использует Java Persistence API (JPA) для реализации ORM. В данном главе мы расскажем о базовых техниках применения JPA и рассмотрим несколько распространнёных случаев, вызывающих затруднение у начинающих работать с JPA. Дополнительную информацию о JPA можно почерпнуть из документации по Hibernate Annotations (реализация JPA, используемая OpenXava по-умолчанию), также вы можете использовать любую другую документацию по JPA.

Связывание объекта (Entity mapping)

Аннотация @Table (Таблица) указывает основную таблицу для связываемого класса сущности (entity). С помощью аннонтации @SecondaryTable (если речь об одной дополнительной таблице) или @SecondaryTables (если нужно указать несколько таблиц) можно указать дополнительные таблицы.
Пример определения сущности:
 @Entity
 @Table(name="CUST", schema="XAVATEST")
 public class Customer {
 
Аттрибут name задает имя таблицы, если оно не соответствует соглашению по наименованию JPA.
Аттрибут schema задает схему, в которой будет размещена таблица.
Аттрибуты не обязательны, полный список аттрибутов смотрите в документации JPA.
Если для сущности не указано определение аннонтации @Table, предполагается, что имя таблицы соответствует соглашениям по наименованию, принятым в JPA.

Связывание свойства (Property mapping)

Аннотация @Column (Столбец) используется для указания столбца таблицы, с которым связывается переменная-член класса сущности. Если для свойства аннотация @Column не определена, то при связывании используются значения и соглашения, принятые в JPA по-умолчанию.
Простейший пример описания свойства:
 @Column(name="DESC", length=512)
 private String description;
 
В примере переопределяется имя столбца аттрибутом name и задается длина данных аттрибутом length.
Пример с аннотированием метода установки значения свойства (getter):
 @Column(name="DESC", nullable=false, length=512)
 public String getDescription() { return description; }
 
Еще примеры:
 @Column(name="DESC",
 columnDefinition="CLOB NOT NULL",
 table="EMP_DETAIL")
 @Lob
 private String description;
 
 @Column(name="ORDER_COST", updatable=false, precision=12, scale=2)
 private BigDecimal cost;
 

Связывание Ссылки (Reference mapping)

Аннотация @JoinColumn (Стоблец, используемый для соединения) используется для определения свойства, которое ссылается на другую сущность.
ПЕР: В примере ниже наша сущность имеет ссылку на Invoice (Счет).
Пример:
 @ManyToOne
 @JoinColumn(name="CUST_ID")
 private Customer customer;
Для связывания с композитным внешним ключом (composite foreign keys) используйте @JoinColumns. Это аннотация позволяет группировать вместе аннотации @JoinColumn, относящиеся к одной ссылке.
При использовании @JoinColumns аттрибуты name и referencedColumnName должны быть определены для каждой аннотации @JoinColumn.
Пример:
 @ManyToOne
 @JoinColumns({
 @JoinColumn(name="INV_YEAR", referencedColumnName="YEAR"),
 @JoinColumn(name="INV_NUMBER", referencedColumnName="NUMBER")
 })
 private Invoice invoice;

Связывание Коллекции (Collection mapping)

При использовании @OneToMany для коллекций обычно нет необходимости делать что-либо дополнительное на второй стороне ассоциации. Но если вы используете @ManyToMany, то возможно будет полезным использовать @JoinTable, как в следующем примере:
 @ManyToMany
 @JoinTable(name="CUSTOMER_STATE",
 joinColumns=@JoinColumn(name="CUSTOMER"),
 inverseJoinColumns=@JoinColumn(name="STATE")
 )
 private Collection<State> states;
 
Если @JoinTable не указан, то применяются значения по-умолчанию.

Связывание Встроенных ссылок (Embedded reference mapping)

Встроенная ссылка (embedded reference) позволяет часть данных выделить в отдельный класс, при этом сохраняя связывание этих данных в ту же физичекую таблицу БД, что и остальные данные основной сущности. Это удобно, когда мы имеем сложные вспомогательные данные, которые мы используем в нескольких сущностях, и не хотим использовать или не можем использовать наследование. Это удобно для вспомогателтельных классов, которые мы используем в нескольких приложениях. Хорошим примером такого подхода является класс Address, содержащий поля адреса. Он может использоваться не только для класса Customer, но и других классов и не только в этом приложении. Такое отделение позволяет облегчить манипулирование адресом как монолитным набором данных. В нашем примере рассмотрена сущность Customer, в которой реализуется встроенная ссылка на Address. Мы используем возможности JPA для реализации этого.
Использование аннотации @AttributeOverrides позволяет нам связать поля таблицы БД корректным образом:
 @Embedded
 @AttributeOverrides({
 @AttributeOverride(name="street", column=@Column("ADDR_STREET")),
 @AttributeOverride(name="zip", column=@Column("ADDR_ZIP"))
 @AttributeOverride(name="city", column=@Column("ADDR_CITY")),
 @AttributeOverride(name="country", column=@Column("ADDR_COUNTRY"))
 })
 private Address address;
Если не использовать @AttributeOverrides, будут применены значения по-умолчанию.

Преобразование типов данных(Type conversion)

Преобразование типов данных от Java к реляционной базе данных и обратно является заботой применяемой реализации JPA (OpenXava использует Hibernate по-умолчанию). Обычно стандартное преобразование типов вполне приемлемо для болшинства случаев, особенно. Тем не менее, иногда, особенно, если вы работаете с уже существующей базой данных, возникает потребность в нестандартном преобразовании данных. Об этом мы расскажем ниже.
В OpenXava мы используем возможности приведения типов, предоставляемые Hibernate. Более подробную информацию можно получить в документации Hibernate .

Преобразование типа свойства (Property conversion)

Если тип данных переменной Java (Java property) и соответствующего столбца в таблице БД не совпадают, то нам необходимо создать Hibernate Type для реализации логики преобразования типов.
Например, если мы в Java хотим работать с типом String [], при этом при передаче в БД мы хотим соединять массив строк в одну строку типа VARCHAR. В этом случае нужно использовать аннотацию @Type для задания преобразования типов следующим образом:
 @Type(type="org.openxava.test.types.RegionsType")
 private String [] regions;
Логика преобразования типов определена в классе RegionsType:
 package org.openxava.test.types;
 
 import java.io.*;
 import java.sql.*;
 
 import org.apache.commons.logging.*;
 import org.hibernate.*;
 import org.hibernate.usertype.*;
 import org.openxava.util.*;
 
 /**
 *
 * @author Javier Paniza
 */
 
 public class RegionsType implements UserType { // 1
 
 public int[] sqlTypes() {
 return new int[] { Types.VARCHAR };
 }
 
 public Class returnedClass() {
 return String[].class;
 }
 
 public boolean equals(Object obj1, Object obj2) throws HibernateException {
 return Is.equal(obj1, obj2);
 }
 
 public int hashCode(Object obj) throws HibernateException {
 return obj.hashCode();
 }
 
 public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) // 2
 throws HibernateException, SQLException
 {
 Object o = resultSet.getObject(names[0]);
 if (o == null) return new String[0];
 String dbValue = (String) o;
 String [] javaValue = new String [dbValue.length()];
 for (int i = 0; i < javaValue.length; i++) {
 javaValue[i] = String.valueOf(dbValue.charAt(i));
 }
 return javaValue;
 }
 
 public void nullSafeSet(PreparedStatement ps, Object value, int index) // 3
 throws HibernateException, SQLException
 {
 if (value == null) {
 ps.setString(index, "");
 return;
 }
 String [] javaValue = (String []) value;
 StringBuffer dbValue = new StringBuffer();
 for (int i = 0; i < javaValue.length; i++) {
 dbValue.append(javaValue[i]);
 }
 ps.setString(index, dbValue.toString());
 }
 
 public Object deepCopy(Object obj) throws HibernateException {
 return obj == null?null:((String []) obj).clone();
 }
 
 public boolean isMutable() {
 return true;
 }
 
 public Serializable disassemble(Object obj) throws HibernateException {
 return (Serializable) obj;
 }
 
 public Object assemble(Serializable cached, Object owner) throws HibernateException {
 return cached;
 }
 
 public Object replace(Object original, Object target, Object owner) throws HibernateException {
 return original;
 }
 
 }
 
Класс для преобразования типов должен реализовывать интерфейс org.hibernate.usertype.UserType (1). Основные методы, которые нужно реализовать, - это nullSafeGet (2), предназначенный для чтения из БД преобразования в тип Java, и nullSafeSet (3) для обратного преобразования.
OpenXava реализует набор готовых преобразователей типов в пакете org.openxava.types . Например, EnumLetterType, который позволяет связать свойство с типом enum. Давайте рассмотрим на примере реализацию свойства с использованием enum:
 private Distance distance;
 public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
 
Стандартное поведение системы таково, что при записи в БД значение 'LOCAL' будет сохранено как 1, 'NATIONAL' как 2 и 'INTERNATIONAL' как 3. Давайте предположим, что нам необходимо, чтобы в БД должны вместо числовых значений сохранятья буквы ('L', 'N' or 'I'). Чтобы реализовать такое сохранение в БД, мы можем использовать тип EnumLetterType:
 @Type(type="org.openxava.types.EnumLetterType",
 parameters={
 @Parameter(name="letters", value="LNI"),
 @Parameter(name="enumType", value="org.openxava.test.model.Delivery$Distance")
 }
 )
 private Distance distance;
 public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
Так как мы указали 'LNI' в качестве значения letters, то преобразователь типов будет предполагать, что 'L' - это 1, 'N' - 2 и 'I' - это 3. Как вы видите из примера, преобразователи типов конфигуруются с помощью своих свойств. Что позволяет их использовать довольно широко.

Связывание одного свойства с несколькими столбцами (Multiple column conversion)

Используя CompositeUserType, мы можем привязать несколько столбцов таблицы БД к одному свойству в Java. Это пригодиться, например. когда ваше свойство в свою очередь является классом, при сохранении которого используется несколько аттрибутов. Так же такое связывание используется, когда мы работаем с базами данных с уже предопределенной структурой (не нами:-)).
Пример класса-конвертора Date3Type. Он реализует преобразование между тремя столбцами в БД и одним свойством типа java.util.Date в Java.
 @Type(type="org.openxava.types.Date3Type")
 @Columns(columns = {
 @Column(name="YEARDELIVERY"),
 @Column(name="MONTHDELIVERY"),
 @Column(name="DAYDELIVERY")
 })
 private java.util.Date deliveryDate;
 
DAYDELIVERY, MONTHDELIVERY и YEARDELIVERY - это 3 стобца в БД, в которых хранится дата доставки. И наконец давайте взглянем на Date3Type:
 package org.openxava.types;
 
 import java.io.*;
 import java.sql.*;
 
 import org.hibernate.*;
 import org.hibernate.engine.*;
 import org.hibernate.type.*;
 import org.hibernate.usertype.*;
 import org.openxava.util.*;
 
 /**
 * In java a <tt>java.util.Date</tt> and in database 3 columns of
 * integer type. <p>
 *
 * @author Javier Paniza
 */
 
 public class Date3Type implements CompositeUserType { // 1
 
 public String[] getPropertyNames() {
 return new String[] { "year", "month", "day" };
 }
 
 public Type[] getPropertyTypes() {
 return new Type[] { Hibernate.INTEGER, Hibernate.INTEGER, Hibernate.INTEGER };
 }
 
 public Object getPropertyValue(Object component, int property) throws HibernateException { // 2
 java.util.Date date = (java.util.Date) component;
 switch (property) {
 case 0:
 return Dates.getYear(date);
 case 1:
 return Dates.getMonth(date);
 case 2:
 return Dates.getYear(date);
 }
 throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
 }
 
 public void setPropertyValue(Object component, int property, Object value)
 throws HibernateException // 3
 {
 java.util.Date date = (java.util.Date) component;
 int intValue = value == null?0:((Number) value).intValue();
 switch (property) {
 case 0:
 Dates.setYear(date, intValue);
 case 1:
 Dates.setMonth(date, intValue);
 case 2:
 Dates.setYear(date, intValue);
 }
 throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
 }
 
 public Class returnedClass() {
 return java.util.Date.class;
 }
 
 public boolean equals(Object x, Object y) throws HibernateException {
 if (x==y) return true;
 if (x==null || y==null) return false;
 return !Dates.isDifferentDay((java.util.Date) x, (java.util.Date) y);
 }
 
 public int hashCode(Object x) throws HibernateException {
 return x.hashCode();
 }
 
 public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
 throws HibernateException, SQLException // 4
 {
 Number year = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[0] );
 Number month = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[1] );
 Number day = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[2] );
 
 int iyear = year == null?0:year.intValue();
 int imonth = month == null?0:month.intValue();
 int iday = day == null?0:day.intValue();
 
 return Dates.create(iday, imonth, iyear);
 }
 
 public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
 throws HibernateException, SQLException // 5
 {
 java.util.Date d = (java.util.Date) value;
 Hibernate.INTEGER.nullSafeSet(st, Dates.getYear(d), index);
 Hibernate.INTEGER.nullSafeSet(st, Dates.getMonth(d), index + 1);
 Hibernate.INTEGER.nullSafeSet(st, Dates.getDay(d), index + 2);
 }
 
 public Object deepCopy(Object value) throws HibernateException {
 java.util.Date d = (java.util.Date) value;
 if (value == null) return null;
 return (java.util.Date) d.clone();
 }
 
 public boolean isMutable() {
 return true;
 }
 
 public Serializable disassemble(Object value, SessionImplementor session)
 throws HibernateException
 {
 return (Serializable) deepCopy(value);
 }
 
 public Object assemble(Serializable cached, SessionImplementor session, Object owner)
 throws HibernateException
 {
 return deepCopy(cached);
 }
 
 public Object replace(Object original, Object target, SessionImplementor session, Object owner)
 throws HibernateException
 {
 return deepCopy(original);
 }
 
 }
 
Как мы говорили выше, класс-конвертор должен реализовывать интерфейс CompositeUserType (1). Ключевыми методами данного класса являются методы getPropertyValue (2) и setPropertyValue (3), которые соответственно применяются для получения (get) и установки (set) значений свойств in the properties of the object of the composite type, and nullSafeGet (4) and nullSafeSet (5) for reading and storing this object from and to database.

Преобразования типа для Ссылки (Reference conversion)

Преобразование типа данных для Ссылки (Reference) не поддерживается напрямую в Hibernate. Иногда в крайне редких обстоятельствах вам все же, возможно, понадобится реализовать таоке пhеобразование. Сейчас мы расскажем, как это сделать.
К примеру, вы используете ссылку на объект "Водительские права" (driver licence), используя два столбца в таблице БД DRIVINGLICENCE_LEVEL и DRIVINGLICENCE_TYPE. При этом столбец DRIVINGLICENCE_TYPE не может иметь значения null, но при том же возможно, что объект не имеет ссылки на driving incense. В таком случае столбец DRIVINGLICENCE_TYPE содержит пустую стоку. Такой дизайн не является хорошей практикой, не используйте его, если сами проектируете БД, используете foreign keys. Но в реальной жизни такое случается, если разработчик БД не умеет обращаться с значениями null.
В таком случае вам необходимо подменять null на пустую строку при работе с DRIVINGLICENCE_TYPE. Это может быть достигнуто следующим образом:
 // We apply conversion (null into an empty String) to DRIVINGLICENCE_TYPE column
 // In order to do it, we create drivingLicence_level and drivingLicence_type
 // We make JoinColumns not insertable nor updatable, we modify the get/setDrivingLincence methods
 // and we create a drivingLicenceConversion() method.
 @ManyToOne(fetch=FetchType.LAZY)
 @JoinColumns({ // 1
 @JoinColumn(name="DRIVINGLICENCE_LEVEL", referencedColumnName="LEVEL",
 insertable=false, updatable=false),
 @JoinColumn(name="DRIVINGLICENCE_TYPE", referencedColumnName="TYPE",
 insertable=false, updatable=false)
 })
 private DrivingLicence drivingLicence;
 private Integer drivingLicence_level; // 2
 private String drivingLicence_type; // 2
 
 public DrivingLicence getDrivingLicence() { // 3
 // In this way because the column for type of driving lincence does not admit null
 try {
 if (drivingLicence != null) drivingLicence.toString(); // to force load
 return drivingLicence;
 }
 catch (EntityNotFoundException ex) {
 return null;
 }
 }
 
 public void setDrivingLicence(DrivingLicence licence) { // 4
 // In this way because the column for type of driving lincence does not admit null
 this.drivingLicence = licence;
 this.drivingLicence_level = licence==null?null:licence.getLevel();
 this.drivingLicence_type = licence==null?null:licence.getType();
 }
 
 @PrePersist @PreUpdate
 private void drivingLicenceConversion() { // 5
 if (this.drivingLicence_type == null) this.drivingLicence_type = "";
 }
 
Во-первых, необходимо использовать аннтотацию @JoinColumns с параметрами insertable=false и updatable=false для всех включенных аннотаций @JoinColumn (1). Как результат свойство-ссылка будет загружаться из БД, но не будет записываться обратно в БД. Также необходимо определить обычное свойсвто для хранения первичного ключа ссылки (2).
Теперь нам нужно написать метод получения значения (getter) getDrivingLicence() (3), который будет возвращать null, если ссылка не найдена, и метод установки значения (setter) setDrivingLicence() (4) для присвоения значения ключа для ссылки в соответствующие обычне свойства.
И наконец нам нужно запрограммировать метод обратного вызова drivingLincenceConversion() (5), чтобы преобразование типов заработало. Данный метод будет автоматически вызываться при операциях создания и изменения (create и update).
Вышеприведенный пример показывает, как можно работать адаптировать OX к работе с уже существующей структурой данных в БД, приложив небольшие усилия в программировании и использовав базовые возможности JPA.